// 
// Entify project
//  
// Author:
//       Lauri Taimila <lauri@taimila.com>
// 
// Copyright (c) 2010 Lauri Taimila
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

using System;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization.Formatters.Binary;

using Mono.Data.Sqlite;

namespace Taimila.Entify.Sqlite
{
	/// <summary>
	/// Contains extension methods for SqliteDataReader type. These
	/// extension methods makes it easier to get data from the reader.
	/// </summary>
	internal static class SqliteDataReaderExtensions
	{
		/// <summary>
		/// Returns string value from given column name.
		/// </summary>
		/// <param name="reader">
		/// Extended object. <see cref="SqliteDataReader"/>
		/// </param>
		/// <param name="columnName">
		/// Column name. <see cref="System.String"/>
		/// </param>
		/// <returns>
		/// Value of the given column. <see cref="System.String"/>
		/// </returns>
		public static string GetString(this SqliteDataReader reader, string columnName)
		{
			try
			{
				if(reader.IsDBNull(reader.GetOrdinal(columnName)))
				{
					return null;
				}
				else
				{
					return reader.GetString(reader.GetOrdinal(columnName));
				}
			}
			catch(Exception)
			{
				throw new ArgumentException("Invalid column name: " + columnName);	
			}
		}
		
		/// <summary>
		/// Returns double value from given columnName.
		/// </summary>
		/// <param name="reader">
		/// Extended object. <see cref="SqliteDataReader"/>
		/// </param>
		/// <param name="columnName">
		/// Column name. <see cref="System.String"/>
		/// </param>
		/// <returns>
		/// Value of the given column. <see cref="System.Double"/>
		/// </returns>
		public static double GetDouble(this SqliteDataReader reader, string columnName)
		{
			try
			{
				return reader.GetDouble(reader.GetOrdinal(columnName));
			}
			catch(Exception)
			{
				throw new ArgumentException("Invalid column name: " + columnName);	
			}
		}
		
		/// <summary>
		/// Returns Int64 value from given columnName.
		/// </summary>
		/// <param name="reader">
		/// Extended object. <see cref="SqliteDataReader"/>
		/// </param>
		/// <param name="columnName">
		/// Column name. <see cref="System.String"/>
		/// </param>
		/// <returns>
		/// Value of the given column. <see cref="System.Double"/>
		/// </returns>
		public static long GetInt64(this SqliteDataReader reader, string columnName)
		{
			try
			{
				return reader.GetInt64(reader.GetOrdinal(columnName));
			}
			catch(Exception)
			{
				throw new ArgumentException("Invalid column name: " + columnName);	
			}
		}
		
		/// <summary>
		/// Returns nullable Int64 value from given columnName.
		/// </summary>
		/// <param name="reader">
		/// Extended object. <see cref="SqliteDataReader"/>
		/// </param>
		/// <param name="columnName">
		/// Column name. <see cref="System.String"/>
		/// </param>
		/// <returns>
		/// Value of the given column. <see cref="System.Double"/>
		/// </returns>
		public static long? GetNullableInt64(this SqliteDataReader reader, string columnName)
		{
			try
			{
				int columnOrdinal = reader.GetOrdinal(columnName);
				if(reader.IsDBNull(columnOrdinal))
				{
					return null;
				}
				else
				{
					return reader.GetInt64(columnOrdinal);
				}
			}
			catch(Exception)
			{
				throw new ArgumentException("Invalid column name: " + columnName);	
			}
		}
		
		/// <summary>
		/// Returns Int32 value from given columnName.
		/// </summary>
		/// <param name="reader">
		/// Extended object. <see cref="SqliteDataReader"/>
		/// </param>
		/// <param name="columnName">
		/// Column name. <see cref="System.String"/>
		/// </param>
		/// <returns>
		/// Value of the given column. <see cref="System.Double"/>
		/// </returns>
		public static int GetInt32(this SqliteDataReader reader, string columnName)
		{
			try
			{
				return reader.GetInt32(reader.GetOrdinal(columnName));
			}
			catch(Exception)
			{
				throw new ArgumentException("Invalid column name: " + columnName);	
			}
		}
		
		/// <summary>
		/// Returns decimal value from given columnName.
		/// </summary>
		/// <param name="reader">
		/// Extended object. <see cref="SqliteDataReader"/>
		/// </param>
		/// <param name="columnName">
		/// Column name. <see cref="System.String"/>
		/// </param>
		/// <returns>
		/// Value of the given column. <see cref="System.Double"/>
		/// </returns>
		public static decimal GetDecimal(this SqliteDataReader reader, string columnName)
		{
			try
			{
				return reader.GetDecimal(reader.GetOrdinal(columnName));
			}
			catch(Exception)
			{
				throw new ArgumentException("Invalid column name: " + columnName);	
			}
		}
		
		/// <summary>
		/// Returns float value from given columnName.
		/// </summary>
		/// <param name="reader">
		/// Extended object. <see cref="SqliteDataReader"/>
		/// </param>
		/// <param name="columnName">
		/// Column name. <see cref="System.String"/>
		/// </param>
		/// <returns>
		/// Value of the given column. <see cref="System.Double"/>
		/// </returns>
		public static float GetFloat(this SqliteDataReader reader, string columnName)
		{
			try
			{
				return reader.GetFloat(reader.GetOrdinal(columnName));
			}
			catch(Exception)
			{
				throw new ArgumentException("Invalid column name: " + columnName);	
			}
		}
		
		/// <summary>
		/// Returns char value from given columnName.
		/// </summary>
		/// <param name="reader">
		/// Extended object. <see cref="SqliteDataReader"/>
		/// </param>
		/// <param name="columnName">
		/// Column name. <see cref="System.String"/>
		/// </param>
		/// <returns>
		/// Value of the given column. <see cref="System.Double"/>
		/// </returns>
		public static char GetChar(this SqliteDataReader reader, string columnName)
		{
			try
			{
				return reader.GetChar(reader.GetOrdinal(columnName));
			}
			catch(Exception)
			{
				throw new ArgumentException("Invalid column name: " + columnName);	
			}
		}
		
		/// <summary>
		/// Returns boolean value from given columnName.
		/// </summary>
		/// <param name="reader">
		/// Extended object. <see cref="SqliteDataReader"/>
		/// </param>
		/// <param name="columnName">
		/// Column name. <see cref="System.String"/>
		/// </param>
		/// <returns>
		/// Value of the given column. <see cref="System.Double"/>
		/// </returns>
		public static bool GetBoolean(this SqliteDataReader reader, string columnName)
		{
			try
			{
				return reader.GetBoolean(reader.GetOrdinal(columnName));
			}
			catch(Exception)
			{
				throw new ArgumentException("Invalid column name: " + columnName);	
			}
		}
		
		/// <summary>
		/// Returns enumeration value from given columnName.
		/// </summary>
		/// <param name="reader">
		/// Extended object. <see cref="SqliteDataReader"/>
		/// </param>
		/// <param name="columnName">
		/// Column name. <see cref="System.String"/>
		/// </param>
		/// <param name="type">
		/// Enum type of the column. <see cref="Type"/>
		/// </param>
		/// <returns>
		/// Value of the given column. <see cref="System.Object"/>
		/// </returns>
		public static object GetEnum(this SqliteDataReader reader, string columnName, Type type)
		{
			try
			{
				string valueName = reader.GetString(reader.GetOrdinal(columnName));
				return Enum.Parse(type, valueName);
			}
			catch(Exception)
			{
				throw new ArgumentException("Invalid column name: " + columnName);	
			}
		}
		
		/// <summary>
		/// Returns deserialized object from given columnName.
		/// </summary>
		/// <param name="reader">
		/// Extended object. <see cref="SqliteDataReader"/>
		/// </param>
		/// <param name="columnName">
		/// Column name. <see cref="System.String"/>
		/// </param>
		/// <returns>
		/// Value of the given column. <see cref="System.Object"/>
		/// </returns>
		public static object GetObject(this SqliteDataReader reader, string columnName)
		{
			if(reader.IsDBNull(reader.GetOrdinal(columnName)))
			{
				return null;
			}
			else
			{
				object obj = null;
				byte[] data = (byte[]) reader.GetValue(reader.GetOrdinal(columnName));
				
				using(MemoryStream stream = new MemoryStream())
	   			{
					stream.Write(data, 0, data.Length);
					stream.Position = 0;
					BinaryFormatter binaryFormatter = new BinaryFormatter();
					obj = binaryFormatter.Deserialize(stream);
				}
				                
				return obj;
			}
		}
		
		/// <summary>
		/// Get entity of given type from the reader.
		/// </summary>
		/// <param name="reader">
		/// Extended object. <see cref="SqliteDataReader"/>
		/// </param>
		/// <returns>
		/// Entity object of given type.
		/// </returns>
		public static T GetEntity<T>(this SqliteDataReader reader)
		{
			Type type = typeof(T);
			T entity = Activator.CreateInstance<T>();

			// Set internal values
			DateTime lastUpdate = DateTime.FromFileTime(reader.GetInt64("Entity Last Update"));
			long entityId = reader.GetInt64("Entity Id");
			(entity as Entity).SetEntityId(entityId);
			(entity as Entity).SetLastModifiedDate(lastUpdate);
			
			foreach(var p in SqlitePersistentProperty.GetPersistentProperties(type))
			{
				if(p.Type == typeof(DateTime))
				{
					DateTime datetime = DateTime.FromFileTime(reader.GetInt64(p.PersistentName));
					type.GetProperty(p.Name).SetValue(entity, datetime, null);
				}
				
				else if(p.Type == typeof(int))
				{
					int number = reader.GetInt32(p.PersistentName);
					type.GetProperty(p.Name).SetValue(entity, number, null);
				}
				
				else if(p.Type == typeof(long))
				{
					long number = reader.GetInt64(p.PersistentName);
					type.GetProperty(p.Name).SetValue(entity, number, null);
				}
				
				else if(p.Type == typeof(decimal))
				{
					decimal number = reader.GetDecimal(p.PersistentName);
					type.GetProperty(p.Name).SetValue(entity, number, null);
				}
				
				else if(p.Type == typeof(double))
				{
					double number = reader.GetDouble(p.PersistentName);
					type.GetProperty(p.Name).SetValue(entity, number, null);
				}
				
				else if(p.Type == typeof(float))
				{
					float number = reader.GetFloat(p.PersistentName);
					type.GetProperty(p.Name).SetValue(entity, number, null);
				}
				
				else if(p.Type == typeof(string))
				{
					string text = reader.GetString(p.PersistentName);
					type.GetProperty(p.Name).SetValue(entity, text, null);
				}
				
				else if(p.Type == typeof(bool))
				{
					bool boolean = reader.GetBoolean(p.PersistentName);
					type.GetProperty(p.Name).SetValue(entity, boolean, null);
				}
				
				else if(p.Type == typeof(char))
				{
					char character = reader.GetString(p.PersistentName)[0];
					type.GetProperty(p.Name).SetValue(entity, character, null);
				}
				
				else if(p.Type == typeof(byte))
				{
					byte number = (byte)reader.GetInt64(p.PersistentName);
					type.GetProperty(p.Name).SetValue(entity, number, null);
				}
				
				else if(p.Type == typeof(sbyte))
				{
					sbyte number = (sbyte)reader.GetInt64(p.PersistentName);
					type.GetProperty(p.Name).SetValue(entity, number, null);
				}
				
				else if(p.Type == typeof(short))
				{
					short number = (short)reader.GetInt64(p.PersistentName);
					type.GetProperty(p.Name).SetValue(entity, number, null);
				}
				
				else if(p.Type == typeof(ushort))
				{
					ushort number = (ushort)reader.GetInt64(p.PersistentName);
					type.GetProperty(p.Name).SetValue(entity, number, null);
				}
				
				else if(p.Type == typeof(uint))
				{
					uint number = (uint)reader.GetInt64(p.PersistentName);
					type.GetProperty(p.Name).SetValue(entity, number, null);
				}
				
				else if(p.Type == typeof(ulong))
				{
					ulong number = (ulong)reader.GetInt64(p.PersistentName);
					type.GetProperty(p.Name).SetValue(entity, number, null);
				}
				else if(p.Type.IsEnum)
				{
					object enumValue = reader.GetEnum(p.PersistentName, p.Type);
					type.GetProperty(p.Name).SetValue(entity, enumValue, null);
				}
				else if(p.Type.BaseType == typeof(Entity))
				{
					long? subEntityId = reader.GetNullableInt64(p.PersistentName);
					string fieldName = SqliteDataReaderExtensions.GetLazyLoadFieldName(p.Name);
					FieldInfo lazyLoadField = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
					lazyLoadField.SetValue(entity, subEntityId);
				}
				else if(p.IsManyRelation)
				{
					// Many-to-many relation properties are skipped since the values are in the different tables.
				}
				else if(p.Type.IsSerializable)
				{
					object obj = reader.GetObject(p.PersistentName);
					type.GetProperty(p.Name).SetValue(entity, obj, null);
				}
				else
				{
					throw new Exception("Unable to create object from SQLite database. Unknown property type: " + p.Type.FullName);	
				}
			}
			
			return entity;
		}
		
		/// <summary>
		/// Get lazyload field name for given property name. Lazyload field
		/// contains entity identifier(s) of the actual value(s) in the property.
		/// </summary>
		/// <param name="propertyName">
		/// Property's name <see cref="System.String"/>
		/// </param>
		/// <returns>
		/// Lazy load field name for given property. <see cref="System.String"/>
		/// </returns>
		private static string GetLazyLoadFieldName(string propertyName)
		{
			string prefix = propertyName[0].ToString().ToLower() + propertyName.Substring(1, propertyName.Length - 1);
			return prefix + "LazyLoadIdentifier";
		}
	}
}
